﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Management.Automation;
using System.Reflection.Emit;

namespace LinqToPowershell
{
    /// <summary>
    /// This class helps you write Powershell cmdlets that both take and emit strongly-typed generic objects, despite Powershell's lack of
    /// native support for generics.  The idea is to store the underlying type arguments of your input -- either discovered at runtime, or provided by 
    /// an explicit parameter -- then create the closed generic type at the last second using reflection.  
    /// </summary>
    public static class GenericHelper
    {        
        /// <summary>
        /// Try to determine the underlying type arguments of the input object, if the input is either
        /// (a) an array (except Object[] doesn't count)
        /// (b) a closed, constructed, generic object
        /// </summary>
        /// <param name="input"></param>
        public static List<Type> GetTypeArgs(this object input)
        {           
            var inputType = input.GetType();
            if (inputType.IsArray)
            {                
                var type = inputType.GetElementType();
                if (type.FullName == "System.Object")
                    return null;

                var typeList = new List<Type>();
                for (int i = 0; i < inputType.GetArrayRank(); ++i )
                {
                    typeList.Add(type);
                }
                return typeList;
            }
            if (inputType.IsGenericType && !inputType.ContainsGenericParameters)
            {
                // this is now guaranteed to return only real types (no generic type definitions)
                return inputType.GetGenericArguments().ToList();
            }       

            return null;
        }

        /// <summary>
        /// Create a generic object of type T(Of typeArgs).  Will throw if things go awry.
        /// </summary>
        /// <typeparam name="T">
        /// T can be a normal, open generic, or closed generic type.  
        /// If generic, it must inherit from a non-generic type of the same name.
        /// </typeparam>
        public static T Create<T>(List<Type> typeArgs, params object[] constructorArgs) where T : class
        {
            return (T) typeof(T).CreateGeneric(typeArgs, constructorArgs);
        }

        /// <summary>
        /// Create a generic object of type myType(Of typeArgs)
        /// </summary>
        public static object CreateGeneric(this Type myType, List<Type> typeArgs, params object[] constructorArgs)
        {            
            // make T an open generic type so we can assign type arguments
            Type openType;
            if (myType.IsGenericType)
            {
                // this handy BCL method turns any generic into an open generic
                openType = myType.GetGenericTypeDefinition();
            }
            else
            {
                // have to roll our own type creation
                var typeName = myType.AssemblyQualifiedName;
                typeName = typeName.Replace(myType.FullName, myType.FullName + "`" + typeArgs.Count);
                openType = Type.GetType(typeName, true);
            }

            var closedType = openType.MakeGenericType(typeArgs.ToArray());
            if (closedType == null)
                throw new ArgumentException(string.Format("Could not create generic {0} with type arg {1}", 
                    myType.ToString(), 
                    typeArgs.Aggregate("", (s, t) => s + ", " + t.ToString())));

            return Activator.CreateInstance(closedType, constructorArgs);
        }

        /// <summary>
        /// Invoke a method on an object.  Assumes there is no ambiguity (overloads, static vs instance, etc)
        /// </summary>
        public static TReturn InvokeMethod<TReturn>(this object obj, string methodName, params object[] methodArgs)
        {
            return (TReturn) obj.InvokeMethod(methodName, methodArgs);
        }
        
        /// <summary>
        /// Invoke a method on an object.  Assumes there is no ambiguity (overloads, static vs instance, etc)
        /// </summary>
        public static object InvokeMethod(this object genericObj, string methodName, params object[] methodArgs)
        {
            var type = genericObj.GetType();
            var method = type.GetMethod(methodName);
            return method.Invoke(genericObj, methodArgs);
        }

        /// <summary>
        /// If not generic already, use CreateGeneric to return a new instance(Of TGenericParam)
        /// </summary>
        public static TMyType ToGeneric<TMyType, TGenericParam>(this TMyType obj, params object[] constructorArgs) where TMyType : class
        {
            return obj.ToGeneric<TMyType>(new List<Type> { typeof(TGenericParam) }, constructorArgs);
        }

        /// <summary>
        /// If not generic already, use CreateGeneric to return a new instance(Of typeArgs)
        /// </summary>
        public static T ToGeneric<T>(this T obj, List<Type> typeArgs, params object[] constructorArgs) where T : class
        {
            return (T) ConvertToGeneric(obj, typeArgs, constructorArgs);
        }

        /// <summary>
        /// If not generic already, use CreateGeneric to return a new instance(Of typeArgs)
        /// </summary>
        public static object ToGeneric(this object obj, List<Type> typeArgs, params object[] constructorArgs)
        {
            return ConvertToGeneric(obj, typeArgs, constructorArgs);
        }

        private static object ConvertToGeneric(object obj, List<Type> typeArgs, object[] constructorArgs)
        {
            var type = obj.GetType();
            if (type.IsGenericType)
                return obj;

            return type.CreateGeneric(typeArgs, constructorArgs);
        }
    }
}
